В статье рассматривается продукт VK Report для ORACLE, который является по своей сути генератором табличных отчетов в популярные на сегодняшний день форматы WinWord RTF (Rich Text Format) и HTML. Генератор оформлен в виде пакета и поэтому процесс построения отчета происходит непосредственно на сервере ORACLE. Легкость, простота, скорость и прелести форматов RTF и HTML - далеко не все преимущества подхода, реализованного автором в пакете VKRep.
Карпов Владислав, TALGAR
Построение отчетов тем способом, которым я предлагаю, в действительности является концепцией. Просто идеей. На каком программном средстве реализована эта идея - не играет никакой принципиальной роли. Генератор отчетов в WinWord RTF и HTML, написанный на PL/SQL для ORACLE - лишнее тому подтверждение. И, естественно, это далеко не первая реализация этой блистательной идеи, пришедшей мне в голову несколько лет назад.
Помимо предоставляемого Вам генератора я, на сегодняшний момент, имею:
-Генератор отчетов в Excel SYLK формат;
-Генератор отчетов в Excel XLS формат 4-ой версии напрямую в файл XLS;
-Генератор отчетов в Excel XLS формат, используя технологию DDE;
-Генератор отчетов в Excel XLS формат, используя технологию COM;
Все это, плюс WinWord RTF и HTML, естественно, реализовано в следующих ипостасях:
-Набор компонент для Delphi;
-Набор компонент для C++Builder;
-Библиотека VKRep.dll с набором экспортируемых функций для систем программирования под Win32;
-Библиотека VKRep.dll как сервер автоматизации;
-Пакет VKRep на PL/SQL для ORACLE сервера (только RTF и HTML);
-Реализация в виде набора функций для DOS CLIPPER (только RTF и HTML)!!!
Последнее выделено потому, что не многие продукты могут похвастаться возможностью построения отчетов под Windows из DOS среды.
Давайте смоделируем процесс построения табличного отчета для самого простого случая, когда по какой-то таблице требуется получить отчет в текстовый файл (вспомним прекрасное время DOS). Итак, что мы делаем на интуитивном уровне? Перво-наперво рисуем в текстовом файле текстовым редактором бланк будущего отчета. Бланк мы размечаем специальным, только нам ведомым способом, разделяя тело бланка на секции. Допустим, что их будет только 3 - заголовок, секция, повторяющаяся для каждой записи таблицы, и замыкающая секция (по научному: REPORT_HEADER, BODY, REPORT_FOOTER - впрочем, обозвать их можно как хотите, но давайте привыкать к тому, как назвал их я). Внутри каждой секции будет какой-то текст и специальным образом определенные метки, вместо которых в будущий отчет встанут данные из таблицы.
Собственно построение отчета заключалось в том, что входной бланк по секциям копировался в выходной файл в цикле по таблице, и если программа копирования натыкалась на метку данных, то вставляла вместо метки информацию из таблицы.
Так делал я. Давно это было. Думаю, что и Вы занимались чем-то подобным.
Ну так вот, господа хорошие, с той поры ничего не изменилось.
Давайте присмотримся к файлу RTF изнутри.
Не будет откровением, если я скажу, что это просто текст. Например, такой:
МАША МЫЛА РАМУ
Если необходимо выделить слово МАША жирным шрифтом, то текст будет выглядеть следующим образом:
{\b МАША} МЫЛА РАМУ
То, что выделено фигурными скобками, называется группой. Группы могут быть вложенными друг в друга неограниченное число раз. Для выделения или форматирования текста используются управляющие слова. В нашем случае управляющее слово \b определяет для группы выделение жирным шрифтом.
RTF файл содержит заголовок, оформленный в виде RTF группы. В заголовке определяются колонтитулы, и он также содержит таблицы цветов и шрифтов, используемых внутри документа.
{\rtf1\ansi\deff1
{\fonttbl
{\f3\fnil Courier;}
{\f1\fnil Times;}}
{\colortbl;
\red255\green0\blue0;
\red0\green255\blue0;
\red0\green0\blue255;}
{\b
МАША} МЫЛА РАМУ}
Все.
Того, что было сказано о внутренностях RTF формата, более чем достаточно.
Структура HTML документа еще более прозрачна, и на ней я вообще останавливаться не буду.
Проблема разметки входного бланка отчета в RTF или HTML формате на секции решается точно таким же образом, как она решалась в старом добром DOS.
{\rtf1\ansi\deff1
{\fonttbl
{\f3\fnil Courier;}
{\f1\fnil Times;}}
{\colortbl;
\red255\green0\blue0;
\red0\green255\blue0;
\red0\green0\blue255;}
{ #BEGIN QREPORT }
{ #BEGIN BODY }
{\b @0001} МЫЛА РАМУ
{ #END BODY }
{
#END QREPORT }}
Для RTF.
<HTML>
<HEAD>
<META HTTP-EQUIV="Content-Type" CONTENT="text/html;
charset=windows-1251">
<META NAME="Generator" CONTENT="Microsoft Word 97">
</HEAD>
<BODY>
{ #BEGIN QREPORT }
{ #BEGIN BODY }
<B> @0001</B> МЫЛА РАМУ
{ #END BODY }
{ #END QREPORT }
</BODY>
Для HTML.
Вы, наверное, уже обратили внимание на появление в бланке вместо имени МАША метки @0001. На это место будут помещаться данные из Вашего источника данных для отчета.
Генератор отчетов копирует входной бланк (неважно, RTF или HTML) в выходной файл байт за байтом, начиная с самого первого, до тех пор, пока не наткнется на открывающую фигурную скобку { #BEGIN QREPORT } (копирование секции FILE_HEADER). Потом перемещает источник данных на первую запись. В цикле копирует секцию BODY (все от закрывающей фигурной скобки { #BEGIN BODY } до открывающей фигурной скобки { #END BODY }). При копировании секций делается проверка на знак @, и, если это метка запроса данных, генератор делает запрос на данные, передавая в качестве параметра номер метки, а принимает строку, которую копирует на место метки. На каждой итерации цикла делается запрос на достижение источником данных конца, и при утвердительном ответе на запрос следует выход из цикла.
Копируется секция FILE_FOOTER (все от закрывающей фигурной скобки { #END QREPORT } до конца входного бланка).
Вот и весь процесс.
На выходе мы получаем что-то типа этого:
МАША МЫЛА РАМУ
КАТЯ МЫЛА РАМУ
НАСТЯ МЫЛА РАМУ
СВЕТА МЫЛА РАМУ
Редактирование входных бланков происходит в два этапа. На первом этапе Вы делаете макет отчета в программах, в которых это делать наиболее удобно. Для RTF это, естественно, Word, для HTML я использую FrontPage. В любом случае Вы вырисовываете будущую бумажку в том виде, в котором хотите ее видеть, благо для этого есть все необходимые средства. Непосредственно в бланке размечаете секции отчета и проставляете метки запросов данных.
На втором этапе происходит незначительная (но, к сожалению, необходимая) доработка бланка в обычном текстовом редакторе на низком уровне. Этот этап необходим, потому что высокоуровневые средства не всегда корректно сохраняют документы с точки зрения генератора отчетов.
Например, генератор ни за что не догадается, где начинается секция BODY, если Word сохранит управляющий тэг секции в виде:
{\v\cf10\lang1033 #BEGIN
{\b BODY}
\par }
Он просто не найдет сочетание #BEGIN BODY.
Еще одной причиной является необходимость наставить перед метками запросов данных управляющие RTF слова, указывающие конкретный русскоязычный шрифт. Word по умолчанию сохраняет метки @0001, @0002, ... в англоязычном шрифте (дурак). Поэтому, если на место такой метки вставить русский текст, то получается абракадабра.
Любой бланк начинается тэгом #BEGIN QREPORT и заканчивается #END QREPORT. Внутри этих двух тэгов Вы определяете секции отчета. Их 11 (не считая FILEHEADER и FILEFOOTER).
REPORT HEADER
GROUP HEADER
SUBGROUP HEADER
SUBSUBGROUP HEADER
SUPPERSUBGROUP HEADER
BODY
REPORT FOOTER
GROUP FOOTER
SUBGROUP FOOTER
SUBSUBGROUP FOOTER
SUPPERSUBGROUP FOOTER
Секция начинается #BEGIN + имя секции, и заканчивается #END + имя секции. Если необходимо, например, определить секцию BODY, то это делается следующим образом:
#BEGIN BODY
... ... @0001 ... ... @0002 ... ...~FieldName~ ...
#END BODY
Внутри секции может быть любой текст, любое форматирование, таблицы, рисунки, и вообще все что угодно. (Рисунки лучше делать по ссылке). Нумерация меток запросов данных в каждой секции начинается с 1. Одна и та же метка может быть поставлена несколько раз. В этом случае генератор несколько раз запросит данные по одной и той же метке.
Исключительно для удобства и для наиболее вероятного попадания тэга отчета в RTF секцию тэги отчета я выделяю специальным образом - какими-нибудь разными цветами и скрытыми шрифтами. Скрытые шрифты удобны, если Вы захотите посмотреть свой бланк БЕЗ управляющих тэгов.
Метки запросов данных могут быть не только номерные, но и символьные - ~FieldName~.
Имена символов заключаются в тильды и могут иметь любые смысловые значения, которые Вы в них вложите на стадии реагирования на запросы данных.
Шапки таблиц для каждой страницы отражаются в колонтитулах. Word может различать колонтитулы первой и последующих страниц, что весьма удобно. Информация о колонтитулах лежит в FILEHEADER-е, и поэтому в колонтитулы тоже можно вделывать метки запросов данных.
При желании Вы можете поместить в бланк фиктивную секцию с какой-то справочной информацией. Такие секции генератор просто игнорирует:
#BEGIN NOTE
Комментарии
#END NOTE
Пока я писал генератор на PL/SQL, я заложил в него возможность обрабатывать ДИНАМИЧЕСКИЕ секции. Это секции с именами, которые Вы даете сами, и Вы же определяете, когда необходимо вывести динамическую секцию в выходной поток.
Вот пример определения динамической секции:
#BEGIN SECTION 'MYSECTION'
... ... @0001 ... ... @0002 ... ...~FieldName~ ...
#END SECTION 'MYSECTION'
Выводится секция (а правильней сказать, должна выводиться) вызовом OutSection('MYSECTION'). К сожалению, пакет DBMS_SQL, при помощи которого реализован механизм реагирования на запросы, не реентерабелен, т.е. не позволяет повторных вхождений. И поэтому возможность вывести динамическую секцию просто отсутствует для пакета VKRep на PL/SQL .
Это печально.
Но не очень.
Если Вас сильно беспокоит отсутствие подобной возможности, попробуйте переписать VKRep с использованием Native dynamic SQL.
Успехов.
Редактирование бланка для HTML отличается лишь тем, что все управляющие тэги для генератора ставятся на низком уровне - в текстовом редакторе.
Воспринимайте этот раздел просто как инструкцию.
1) Сделайте резервную копию бланка.
2) Загрузите бланк любым текстовым редактором (я люблю MultiEdit).
3) Найдите при помощи стандартных механизмов поиска в редакторе все вхождения символа #.
4) Проанализируйте эти вхождения, если это тэги для генератора VKRep, на предмет возможности распознать их генератором. Если попадаются варианты типа:
{\v\cf10\lang1033\langfe1033\langnp1033 #BEGIN GROUP
{\lang1033 HEADER }
\par }
То их нужно РУКАМИ привести к виду:
{\v\cf10\lang1033\langfe1033\langnp1033 #BEGIN GROUP HEADER
\par }
Т.е. просто взять и выкинуть лишнее.
Основное требование - чтобы #BEGIN GROUP HEADER было именно таким, как написано, поскольку это выражение является ключом поиска. Второе требование - чтобы внутри RTF группы с определением тэга секции не было других RTF групп. Т.е. такое определение группы с секцией для отчета
{\v\cf10\lang1033\langfe1033\langnp1033 #BEGIN GROUP HEADER
{\b }
\par }
недопустимо.
5) Найдите при помощи стандартных механизмов поиска в редакторе все вхождения символа @.
6) Проанализируйте метки запросов данных на неразрывность. Выражения типа @0 {\b 0}01 недопустимы. Поправьте их тоже руками до вида @0001.
7) Найдите в RTF документе описание кириллического шрифта. Например:
{\f31\froman\fcharset204\fprq2 Times New Roman Cyr;}
(Я это делаю обычным поиском по вхождению слова Cyr). Идентификатор \f31 по всему документу будет определять именно этот шрифт. К сожалению, Word перед метками при сохранении документов не ставит никаких шрифтов (что значит "взять шрифт по умолчанию"), даже если конкретно указать шрифт для метки. Поэтому приходится это делать вручную. Сделать это можно двумя способами. Либо просто поменять шрифт по умолчанию на кириллический, либо перед каждой меткой запроса данных поставить идентификатор шрифта.
Я обычно пользуюсь вторым способом.
Делаю я это путем поиска и замены "@" на "\f31 @".
Первый способ состоит в том, чтобы поменять шрифт по умолчанию. Для этого необходимо заменить
\fcharset0
на
\fcharset204
На самом деле это проще и лучше.
8) Сохраните документ при выходе из текстового редактора.
9) Попробуйте загрузить получившийся бланк в Word. Если удачно, то ВСЕ. Подготовка бланка закончена.
Если неудачно - у Вас есть резервная копия.
10) Для HTML все совсем тривиально. После макетирования бланка отчета в какой-либо сильной программулине (FrontPage, например) Вы входите в текст HTML текстовым редактором и просто вручную проставляете всю необходимую разметку для генератора и метки запросов данных, руководствуясь следующими простыми правилами:
- Оставляйте пробелы между фигурными скобками и названиями тэгов ({ #BEGIN BODY });
- Не делайте монолитных таблиц - они очень долго засасываются браузером. Разделения таблиц лучше всего делать в стыках разделения групп;
- Это все правила.
Если не рассматривать всякую шушеру, до которой Вы, несомненно, докопаетесь (но потом), то можно считать, что пакет VKRep состоит из единственной функции:
PROCEDURE ReportExecute(pCallBackProcName IN VARCHAR2, pInputBlank IN CLOB, pOutputReport IN OUT CLOB);
Функция принимает 3 параметра. Первый параметр - это символьное имя функции обратного вызова, через которую, собственно, проходят все события при работе по построению отчета. Эта функция прописывается Вами - и она и есть ПОСТРОЕНИЕ ОТЧЕТА.
Оставшиеся 2 параметра - CLOB значения с входным бланком и выходным отчетом.
Функция ReportExecute производит разбор входного бланка с целью выявления секций отчета, после чего запускает цикл, в котором поочередно пытается вывести в выходной CLOB определенные Вами секции.
На этом этапе происходит два важнейших события - генератор через определенную Вами функцию обратного вызова сообщает перед входом в цикл поочередного копирования секций о том, что необходимо перевести Ваш источник данных (скорее всего, открытый курсор, хотя и не обязательно) на начало. Вы должны ответить при этом, достигнут ли конец данных.
При входе в цикл копирования генератор через ту же функцию обратного вызова делает запросы на перемещение источника данных. При этом Вы опять должны указать достижение конца данных. И если таковое имеет место, то процесс копирования заканчивается, и генератор выходит из цикла.
Функция обратного вызова, через которую проходят ВСЕ запросы, должна иметь следующий прототип:
PROCEDURE <Имя процедуры>( pMessage IN INTEGER,
pW IN INTEGER,
pL IN INTEGER);
Как видите, очень напоминает оконную процедуру, хорошо знакомую программерам под Windows. И это естественно - первая реализация моего генератора была написана на С, и в качестве функции обратного вызова использовалась оконная процедура какого-либо окна на клиентском приложении.
В первом параметре посылается номер сообщения. Все сообщения, которые посылает генератор, имеют мнемоническое имя, описанное в пакете. Например, сообщение о переводе источника данных имеет имя VKREP.WM_REP_MOVE, при переводе источника данных на следующую запись в параметре pL посылается константа MOVE_NEXT, на начало источника данных - MOVE_TOP.
Это не единственные два события, которые генерирует тело процедуры ReportExecute. В схеме событий присутствуют такие как WM_REP_START_REPORT и WM_REP_END_REPORT, которые определяют моменты времени начала и завершения построения выходного CLOB-а.
Вышеописанную схему перемещения по данным можно представить диаграммой:
В цикле происходит копирование секций. При копировании одной секции через функцию обратного вызова проходят сообщения, отраженные в схеме:
Копирование секции начинается с уведомления о том, что генератор приступил к копированию секции ( SECTION_BEGIN ). Следующее событие ( SECTION_CHECK ) спрашивает, а копировать ли вообще данную секцию. Секция BODY по умолчанию копируется всегда, чего не скажешь, например, о секции GROUP_FOOTER, поэтому именно в событии SECTION_CHECK Вы должны указать конкретно, когда копировать подобные секции - по изменению ключевого выражения для группы.
Если событие ( SECTION_CHECK ) выдало положительный результат, происходит копирование, в процессе которого генератор может натыкаться на метки запросов данных, поэтому следующим событием будет событие запроса данных по меткам. Результатами отработки таких событий будут сами данные для отчета.
Следующее событие ( SECTION_END ) уведомляет о том, что процесс копирования секции отчета завершен. Это сообщение проходит только в том случае, если генератор вообще копировал эту секцию.
Завершающее нотификационное сообщение SECTION_COMPLIT проходит в любом случае - не в зависимости от того, было копирование или его не было.
Для того, чтобы сделать отчет, лучшим способом является написать пакет. В пакете Вы определяете источник данных. В примере это курсор:
CURSOR C_EMP IS
select
*
from
emp, dept
where
emp.deptno = dept.deptno
order by
emp.job;
V_EMP C_EMP%ROWTYPE;
И функцию обратного вызова для реагирования на запросы генератора отчетов:
PROCEDURE ExecuteReport(pMessage IN INTEGER, pW IN INTEGER, pL IN INTEGER);
Как видите, функция имеет формат, который мы обсуждали ранее. Для того, чтобы запустить генератор строить отчет, необходимо открыть курсор и вызвать процедуру пакета VKREP ReportExecute:
PROCEDURE StartReport IS
BEGIN
Test1.OpenSource;
VKREP.ReportExecute('Test1.ExecuteReport', InputBlank, OutputReport);
Test1.CloseSource;
END;
Для удобства это оформляется в виде отдельной процедуры.
При вызове ReportExecute в качестве параметров ей передается строка с полным именем (с указанием названия пакета) функции обратного вызова и два LOB - а. С бланком для отчета и выходным отчетом.
Как Вы понимаете, все самое интересное происходит именно в функции обратного вызова.
Рассмотрим события, через нее проходящие, более подробно.
Событие VKREP.WM_REP_MOVE:
IF pMessage = VKREP.WM_REP_MOVE THEN
IF pL = VKREP.MOVE_TOP THEN
PriorJob := '';
ELSE
PriorJob := V_EMP.JOB;
END IF;
FETCH C_EMP INTO V_EMP;
VKREP.ucCHECK := C_EMP%NOTFOUND;
END IF;
Возникает при итерациях цикла копирования. В параметре pL приходят константы VKREP.MOVE_TOP и VKREP.MOVE_NEXT. В обработчике считываются новые данные из курсора и заполняется переменная пакета VKREP.ucCHECK , которая служит флажком для выхода из цикла.
При копировании секции HEADER возникают два события запроса данных, потому что во входном бланке две метки запроса данных:
IF pMessage = VKREP.WM_REP_HEADER THEN
IF pL = VKREP.SECTION_DATA_REQUEST_BY_NUM THEN
IF pW = 1 THEN
VKREP.RequestStr := 'First parametr.';
ELSIF pW = 2 THEN
VKREP.RequestStr := 'Second parametr.';
END IF;
END IF;
END IF;
Для GROUP_HEADER нужно определить, когда выводить этот GROUP_HEADER, и ответить на запросы данных по номерам и по именам:
IF pMessage = VKREP.WM_REP_GROUP_HEADER THEN
IF pL = VKREP.SECTION_CHECK THEN
IF PriorJob <> V_EMP.JOB THEN
VKREP.ucCHECK := TRUE;
END IF;
END IF;
IF pL = VKREP.SECTION_DATA_REQUEST_BY_NAME THEN
VKREP.RequestStr :=
LTRIM(RTRIM(V_EMP.JOB));
END IF;
IF pL = VKREP.SECTION_DATA_REQUEST_BY_NUM THEN
IF pW = 1 THEN
SELECT AVG(SAL) INTO AVG_S FROM EMP WHERE JOB = V_EMP.JOB;
VKREP.RequestStr := TO_CHAR(round(AVG_S, 2));
END IF;
END IF;
END IF;
Таким образом, бланк вида blank.rtf аккуратно превращается в отчет вида Report.rtf
Это, собственно, все, что я хотел сказать.
Спасибо за внимание.
С уважением, ведущий специалист компании Talgar Владислав Карпов.